﻿/*
 * author: Q_464872327 - alone_xb
 *
 * PS: 支持 Particle 、MeshRenderer、SkinndRenderer、粒子 textureSheet，当作为 UI对象时候，请在 Canvas下制作特效，因为 UGUI 在 Canvas下有一定的缩放比例
 * 
 */

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;
using Random = UnityEngine.Random;

namespace UGUIEffect.UIParticles
{
    public enum UIParticleRenderMode
    {
        Billboard,
        StreachedBillboard,
        Mesh
    }
    
    [RequireComponent(typeof(RectTransform), typeof(CanvasRenderer))]
    public class UIParticles : MaskableGraphic
    {
        #region Fileds

        [SerializeField] private ParticleSystem m_ParticleSystem;
        [SerializeField] private MeshFilter m_MeshFilter;
        [SerializeField] private SkinnedMeshRenderer m_SkinnedMeshRenderer;

        [SerializeField, Tooltip("网格是否使用粒子系统配置的颜色")] private bool m_MeshUseParticleColor;
        [SerializeField, Tooltip("粒子的渲染模式")] private UIParticleRenderMode m_RendererMode;
        [SerializeField, Tooltip("标度粒度，取决于粒子速度")] private float m_StretchedSpeedScale;
        [SerializeField, Tooltip("拉伸模式下的广告牌粒长度")] private float m_StretchedLenghScale;
        [SerializeField, Tooltip("缩放")] private float m_Scale;
        [SerializeField, Tooltip("蒙皮网格")] private Mesh m_SkinndMesh;
        [SerializeField, Tooltip("Mesh Filter 上的网格")] private Mesh m_MFilterMesh;
        [SerializeField, Tooltip("一般用于静态网格（不会发生形变的网格）")] private bool m_StaticRefresh;
        [SerializeField, Tooltip("如果为true，则粒子将忽略时间刻度")] private bool m_IgnoreTimescale = false;
        
        private bool m_Refresh = false;
        private Mesh m_CalcMesh;
        private Vector3 m_LeftTop;
        private Vector3 m_RightTop;
        private Vector3 m_RightBottom;
        private Vector3 m_LeftBottom;
        
        private Vector3 leftTop;
        private Vector3 rightTop;
        private Vector3 rightBottom;
        private Vector3 leftBottom;
        
        private List<Matrix4x4>     m_BindposesMatrix4x4   =     new List<Matrix4x4>();
        private List<BoneWeight>    m_BoneWeights          =     new List<BoneWeight>();
        private List<Vector2>       m_Uvs                  =     new List<Vector2>();
        private List<Vector3>       m_Vertices             =     new List<Vector3>();
        private List<Color>         m_Colors               =     new List<Color>();
        private List<int>           m_Triangles            =     new List<int>();
        private Vector2[]           m_UvsArray             =     new Vector2[4];
        private Transform[]         m_BonesTransforms      =     null;

        private ParticleSystemRenderer m_ParticleSystemRenderer = null;
        private ParticleSystem.Particle[] m_Particles = null;
        
        #region Propertys

        public ParticleSystem ParticleSystemPro
        {
            get { return this.m_ParticleSystem;}
            set
            {
                if (SetPropertyUtility.SetClass(ref m_ParticleSystem, value))
                    SetAllDirty();
            }
        }

        public override Texture mainTexture
        {
            get
            {
                if (material != null && material.HasProperty("_MainTex"))
                {
                    if (material.mainTexture != null)
                    {
                        return material.mainTexture;
                    }
                }
                else
                {
                    if (material != null)
                        Debug.LogError($"Not Find _MainTex {material.name}");
                }

                return s_WhiteTexture;
            }
        }
        
        public UIParticleRenderMode RenderMode
        {
            get { return this.m_RendererMode; }
            set
            {
                if (SetPropertyUtility.SetStruct(ref this.m_RendererMode, value))
                    SetAllDirty();
            }
        }

        public float Scale
        {
            get
            {
                return this.m_Scale;
            }
        }

        #endregion
        
        
        #endregion

        protected override void Awake()
        {
            this.m_ParticleSystem = GetComponent<ParticleSystem>();
            this.m_ParticleSystemRenderer = GetComponent<ParticleSystemRenderer>();
            this.m_MeshFilter = GetComponent<MeshFilter>();
            this.m_SkinnedMeshRenderer = GetComponent<SkinnedMeshRenderer>();

            if (this.m_Material == null)
            {
                if (this.m_ParticleSystemRenderer != null)
                {
                    m_Material = this.m_ParticleSystemRenderer.sharedMaterial;
                }
                else if (this.m_SkinnedMeshRenderer != null)
                {
                    m_Material = this.m_SkinnedMeshRenderer.sharedMaterial;

                    this.m_SkinnedMeshRenderer.shadowCastingMode = ShadowCastingMode.Off;
                    this.m_SkinnedMeshRenderer.receiveShadows = false;
                    this.m_SkinnedMeshRenderer.lightProbeUsage = LightProbeUsage.Off;
                    this.m_SkinnedMeshRenderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
                }
                else
                {
                    Renderer r = this.GetComponent<Renderer>();
                    if (r)
                    {
                        m_Material = r.sharedMaterial;
                    }
                }
            }
            
            if (this.m_ParticleSystemRenderer != null)
            {
                if (this.m_ParticleSystemRenderer.renderMode == ParticleSystemRenderMode.Stretch)
                    RenderMode = UIParticleRenderMode.StreachedBillboard;
            }
            base.Awake();
            
            // disable default particle renderer, we using our custom
            if (this.m_ParticleSystemRenderer != null && this.m_ParticleSystemRenderer.enabled)
                this.m_ParticleSystemRenderer.enabled = false;
            this.raycastTarget = false;
            if (this.m_MeshFilter || this.m_SkinnedMeshRenderer)
            {
                this.m_RendererMode = UIParticleRenderMode.Mesh;
            }
        }

        public override void SetMaterialDirty()
        {
            base.SetMaterialDirty();
            if (this.m_ParticleSystemRenderer != null)
            {
                this.material = this.m_ParticleSystemRenderer.sharedMaterial;
            }
        }

        protected override void OnPopulateMesh(VertexHelper toFill)
        {
            if (this.m_ParticleSystem == null && !this.m_SkinnedMeshRenderer && !this.m_MFilterMesh)
            {
                base.OnPopulateMesh(toFill);
                return;
            }
            this.GenerateParticlesBillboards(toFill);
        }

        private void InitParticlesBuffer()
        {
            if (this.m_ParticleSystem)
            {
                if (this.m_Particles == null || this.m_Particles.Length < this.m_ParticleSystem.main.maxParticles)
                    this.m_Particles = new ParticleSystem.Particle[this.m_ParticleSystem.main.maxParticles];
            }
        }
        private void GenerateParticlesBillboards(VertexHelper toFill)
        {
            this.InitParticlesBuffer();
            
            toFill.Clear();

            if (this.m_ParticleSystem)
            {
                int numParticlesAlive = this.m_ParticleSystem.GetParticles(this.m_Particles);

                for (int i = 0; i < numParticlesAlive; i++)
                {
                    
                    
                    if (this.m_MeshFilter || this.m_SkinnedMeshRenderer || this.m_ParticleSystemRenderer.renderMode == ParticleSystemRenderMode.Mesh)
                    {
                        DrawParticleMeshBoard(this.m_Particles[i], toFill);
                    }
                    else
                    {
                        DrawParticleBillboard(m_Particles[i], toFill, numParticlesAlive, i);
                    }
                }

            }
            else
            {
                if (this.m_MeshFilter || this.m_SkinnedMeshRenderer || this.m_ParticleSystemRenderer.renderMode == ParticleSystemRenderMode.Mesh)
                {
                    DrawParticleMeshBoard(new ParticleSystem.Particle(), toFill);
                }
            }
            
        }
        
        private void DrawParticleBillboard(ParticleSystem.Particle particle, VertexHelper toFill, int numParticlesAlive, int index)
        {
            this.m_CalcMesh = this.m_ParticleSystemRenderer.mesh;

            var center = particle.position;
            var rotation = Quaternion.Euler(particle.rotation3D);
            Vector3 size3D = particle.GetCurrentSize3D(this.m_ParticleSystem);
            Color32 color32 = particle.GetCurrentColor(this.m_ParticleSystem);
            
            if (this.m_ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World)
            {
                center = rectTransform.InverseTransformPoint(center);
            }
            
            float timeAlive = particle.startLifetime - particle.remainingLifetime;
            float globalTimeAlive = timeAlive / particle.startLifetime;

            if (this.m_RendererMode == UIParticleRenderMode.StreachedBillboard)
            {
                GetStrechedBillboardsSizeAndRotation(particle, globalTimeAlive, ref size3D, out rotation);
            }

            this.m_LeftTop.Set(-size3D.x * 2, size3D.y * 2, 0);
            this.m_RightTop.Set(size3D.x * 2, size3D.y * 2, 0);
            this.m_RightBottom.Set(size3D.x * 2, -size3D.y * 2, 0);
            this.m_LeftBottom.Set(-size3D.x * 2, -size3D.y * 2, 0);
        
        
            this.m_LeftTop = rotation * this.m_LeftTop * this.m_Scale + center;
            this.m_RightTop = rotation * this.m_RightTop * this.m_Scale + center;
            this.m_RightBottom = rotation * this.m_RightBottom * this.m_Scale + center;
            this.m_LeftBottom = rotation * this.m_LeftBottom * this.m_Scale + center;
        


            var i = toFill.currentVertCount;
            if (!this.m_ParticleSystem.textureSheetAnimation.enabled)
                EvaluateQuadUVs(this.m_UvsArray);
            else
                EvaluateTexturesheetUVs(particle, timeAlive, this.m_UvsArray);
        
            toFill.AddVert(this.m_LeftBottom, color32, this.m_UvsArray[0]);
            toFill.AddVert(this.m_LeftTop, color32, this.m_UvsArray[1]);
            toFill.AddVert(this.m_RightTop, color32, this.m_UvsArray[2]);
            toFill.AddVert(this.m_RightBottom, color32, this.m_UvsArray[3]);

            toFill.AddTriangle(i, i + 1, i + 2);
            toFill.AddTriangle(i + 2, i + 3, i);
        }

        private void EvaluateTexturesheetUVs(ParticleSystem.Particle particle, float timeAlive, Vector2[] uvs)
        {
            var textureAnimator = this.m_ParticleSystem.textureSheetAnimation;

            float lifeTimePerCycle = particle.startLifetime / textureAnimator.cycleCount;
            float timePerCycle = timeAlive % lifeTimePerCycle;
            float timeAliveAnim01 = timePerCycle / lifeTimePerCycle; // in percents


            var totalFramesCount = textureAnimator.numTilesY * textureAnimator.numTilesX;
            var frame01 = textureAnimator.frameOverTime.Evaluate(timeAliveAnim01);

            var frame = 0f;
            switch (textureAnimator.animation)
            {
                case ParticleSystemAnimationType.WholeSheet:
                    {
                        frame = Mathf.Clamp(Mathf.Floor(frame01 * totalFramesCount), 0, totalFramesCount - 1);
                        break;
                    }
                case ParticleSystemAnimationType.SingleRow:
                    {
                        frame = Mathf.Clamp(Mathf.Floor(frame01 * textureAnimator.numTilesX), 0, textureAnimator.numTilesX - 1);
                        int row = textureAnimator.rowIndex;
                        bool userowMode = false;
#if UNITY_2017
                        userowMode = textureAnimator.useRandomRow;
#elif UNITY_2017_4_OR_NEWER
                        userowMode = textureAnimator.rowMode == ParticleSystemAnimationRowMode.Random;
#endif
                        if (userowMode)
                        {
                            Random.InitState((int)particle.randomSeed);
                            row = Random.Range(0, textureAnimator.numTilesY);
                        }
                        frame += row * textureAnimator.numTilesX;
                        break;
                    }
            }

            int x = (int)frame % textureAnimator.numTilesX;
            int y = (int)frame / textureAnimator.numTilesX;


            var xDelta = 1f / textureAnimator.numTilesX;
            var yDelta = 1f / textureAnimator.numTilesY;
            y = textureAnimator.numTilesY - 1 - y;
            var sX = x * xDelta;
            var sY = y * yDelta;
            var eX = sX + xDelta;
            var eY = sY + yDelta;

            uvs[0] = new Vector2(sX, sY);
            uvs[1] = new Vector2(sX, eY);
            uvs[2] = new Vector2(eX, eY);
            uvs[3] = new Vector2(eX, sY);
        }

        private void EvaluateQuadUVs(Vector2[] uvs)
        {
            uvs[0].Set(0f, 0f);
            uvs[1].Set(0f, 1f);
            uvs[2].Set(1f, 1f);
            uvs[3].Set(1f, 0f);
        }

        private void DrawParticleMeshBoard(ParticleSystem.Particle particle, VertexHelper toFill)
        {
            if (this.m_ParticleSystemRenderer != null)
            {
                this.m_CalcMesh = this.m_ParticleSystemRenderer.mesh;
            }
            else
            {
                if (this.m_RendererMode == UIParticleRenderMode.Mesh)
                {
                    if (this.m_MeshFilter && this.m_MeshFilter.sharedMesh && this.m_MFilterMesh == null)
                    {
                        this.m_MFilterMesh = this.m_MeshFilter.sharedMesh;
                        this.m_CalcMesh = this.m_MFilterMesh;
                    }
                    else
                    {
                        if (this.m_MFilterMesh)
                            this.m_CalcMesh = this.m_MFilterMesh;
                    }

                    if (this.m_SkinnedMeshRenderer && this.m_SkinnedMeshRenderer.sharedMesh && this.m_SkinndMesh == null)
                    {
                        this.m_SkinndMesh = this.m_SkinnedMeshRenderer.sharedMesh;
                        this.m_CalcMesh = this.m_SkinndMesh;
                    }
                    else
                    {
                        if (this.m_SkinndMesh)
                            this.m_CalcMesh = this.m_SkinndMesh;
                    }

                    if (this.m_SkinnedMeshRenderer)
                    {
                        this.m_CalcMesh.GetBoneWeights(this.m_BoneWeights);
                        this.m_CalcMesh.GetBindposes(this.m_BindposesMatrix4x4);
                        this.m_BonesTransforms = this.m_SkinnedMeshRenderer.bones;
                    }

                    if (Application.isPlaying)
                    {
                        if (this.m_MeshFilter)
                            this.m_MeshFilter.sharedMesh = null;
                        if (this.m_SkinnedMeshRenderer)
                            this.m_SkinnedMeshRenderer.sharedMesh = null;
                    }
                }
            }
            
            if (this.m_CalcMesh == null) return;
            this.m_CalcMesh.GetVertices(this.m_Vertices);
            this.m_CalcMesh.GetUVs(0, this.m_Uvs);
            this.m_CalcMesh.GetColors(this.m_Colors);
            this.m_CalcMesh.GetTriangles(this.m_Triangles, 0);
            float sizeScale = 0;
            
            if (this.m_ParticleSystem != null)
            {
                sizeScale = particle.GetCurrentSize(this.m_ParticleSystem) * this.m_Scale;
            }
            else
            {
                sizeScale = this.m_Scale;
            }

            this.SpwanVertices(toFill, sizeScale, particle);
            this.SpwanTriangles(toFill);

        }

        private void SpwanVertices(VertexHelper toFill, float sizeScale, ParticleSystem.Particle particle)
        {
            for (int i = 0; i < this.m_Vertices.Count; i++)
            {
                try
                {
                    if (this.m_ParticleSystem == null)
                    {
                        if (this.m_Colors.Count > 0)
                        {
                            toFill.AddVert(this.m_Vertices[i] * sizeScale, this.m_Colors[i], this.m_Uvs[i]);
                        }
                        else if (this.m_SkinnedMeshRenderer == null)
                        {
                            toFill.AddVert(this.m_Vertices[i] * sizeScale, this.color, this.m_Uvs[i]);
                        }
                        else
                        {
                            BoneWeight boneWeight = this.m_BoneWeights[i];
                            Vector3 point = this.m_Vertices[i];

                            Transform trans0 = this.m_BonesTransforms[boneWeight.boneIndex0];
                            Transform trans1 = this.m_BonesTransforms[boneWeight.boneIndex1];
                            Transform trans2 = this.m_BonesTransforms[boneWeight.boneIndex2];
                            Transform trans3 = this.m_BonesTransforms[boneWeight.boneIndex3];

                            Matrix4x4 tempMat0 = trans0.localToWorldMatrix * this.m_BindposesMatrix4x4[boneWeight.boneIndex0];
                            Matrix4x4 tempMat1 = trans1.localToWorldMatrix * this.m_BindposesMatrix4x4[boneWeight.boneIndex1];
                            Matrix4x4 tempMat2 = trans2.localToWorldMatrix * this.m_BindposesMatrix4x4[boneWeight.boneIndex2];
                            Matrix4x4 tempMat3 = trans3.localToWorldMatrix * this.m_BindposesMatrix4x4[boneWeight.boneIndex3];

                            Vector3 temp = tempMat0.MultiplyPoint(point) * boneWeight.weight0 +
                                           tempMat1.MultiplyPoint(point) * boneWeight.weight1 +
                                           tempMat2.MultiplyPoint(point) * boneWeight.weight2 +
                                           tempMat3.MultiplyPoint(point) * boneWeight.weight3;

                            this.m_Vertices[i] = this.m_SkinnedMeshRenderer.worldToLocalMatrix.MultiplyPoint(temp);
                            toFill.AddVert(this.m_Vertices[i] * sizeScale, Color.white, this.m_Uvs[i]);
                        }
                    }
                    else
                    {
                        if (this.m_Colors.Count > 0 && !this.m_MeshUseParticleColor)
                        {
                            toFill.AddVert(this.m_Vertices[i] * sizeScale, this.m_Colors[i], this.m_Uvs[i]);
                        }
                        else
                        {
                            toFill.AddVert(this.m_Vertices[i] * sizeScale, particle.GetCurrentColor(this.m_ParticleSystem), this.m_Uvs[i]);
                        }
                    }
                }
                catch (System.Exception)
                {

                    Debug.LogError($"顶点颜色信息不匹配 {this.m_Vertices.Count}，{this.m_Colors.Count}，{this.m_Uvs.Count}");
                }
            }
        }

        private void SpwanTriangles(VertexHelper toFill)
        {
            for (int i = 0; i < this.m_Triangles.Count; i++)
            {
                try
                {
                    toFill.AddTriangle(this.m_Triangles[i], this.m_Triangles[i + 1], this.m_Triangles[i + 2]);
                    i += 2;
                }
                catch (System.Exception ex)
                {
                    Debug.LogError(ex.Message);
                }
            }
        }
        
        
        /// <summary>
        /// 在拉伸的广告牌模式下评估粒子的大小和旋转
        /// </summary>
        /// <param name="particle">particle</param>
        /// <param name="timeAlive01">current life time percent [0,1] range</param>
        /// <param name="size3D">particle size</param>
        /// <param name="rotation">particle rotation</param>
        public void GetStrechedBillboardsSizeAndRotation(ParticleSystem.Particle particle, float timeAlive01,
            ref Vector3 size3D, out Quaternion rotation)
        {
            var velocityOverLifeTime = Vector3.zero;

            if (this.m_ParticleSystem.velocityOverLifetime.enabled)
            {
                velocityOverLifeTime.x = this.m_ParticleSystem.velocityOverLifetime.x.Evaluate(timeAlive01);
                velocityOverLifeTime.y = this.m_ParticleSystem.velocityOverLifetime.y.Evaluate(timeAlive01);
                velocityOverLifeTime.z = this.m_ParticleSystem.velocityOverLifetime.z.Evaluate(timeAlive01);
            }

            var finalVelocity = particle.velocity + velocityOverLifeTime;
            var ang = Vector3.Angle(finalVelocity, Vector3.up);
            var horizontalDirection = finalVelocity.x < 0 ? 1 : -1;
            rotation = Quaternion.Euler(new Vector3(0, 0, ang * horizontalDirection));
            size3D.y *= this.m_StretchedLenghScale;
            size3D += new Vector3(0, this.m_StretchedSpeedScale * finalVelocity.magnitude);
        }

        protected virtual void Update()
        {
            if (this.m_StaticRefresh == false)
                this.m_Refresh = false;
            if (this.m_Refresh)
            {
                return;
            }
            if (!this.m_IgnoreTimescale)
            {
                if ((this.m_ParticleSystem != null && this.m_ParticleSystem.isPlaying) || this.m_MeshFilter != null || this.m_SkinnedMeshRenderer != null)
                {
                    SetAllDirty();
                }
            }
            else
            {
                if (this.m_ParticleSystem != null || this.m_MeshFilter != null || this.m_SkinnedMeshRenderer != null)
                {
                    this.m_ParticleSystem.Simulate(Time.unscaledDeltaTime, true, false);
                    SetAllDirty();
                }
            }
            if (this.m_StaticRefresh == true && m_Refresh == false)
            {
                this.m_Refresh = true;
            }
        }
    }
}


